package redis

import (
	"time"

	"sync"

	"errors"

	"github.com/go-redis/redis"
)

var instances map[string]*RedisCache

var rwInstanceLock *sync.RWMutex
var rwClientsLock *sync.RWMutex

var connectionCapacity = 16

func init() {
	instances = make(map[string]*RedisCache)
	rwInstanceLock = new(sync.RWMutex)
	rwClientsLock = new(sync.RWMutex)
}

type clientPoolUnit struct {
	inUse  bool
	client *redis.Client
}

func (c *clientPoolUnit) release() {
	c.inUse = false
}

type RedisCache struct {
	configs      RedisConfigCluster
	hash         ConsistentHash
	targetToPool map[string][]*clientPoolUnit
}

// 初始化配置
func Init(configNameMap map[string]string) {
	// 初始化需要
	if IsFirstInit() {
		ConfigNameMap = configNameMap
		ch := make(chan bool)
		go AsyncDove(ch)
		<-ch
	}
}

func GetInstance(configName string) (rCache *RedisCache) {
	rwInstanceLock.RLock()
	rCache, ok := instances[configName]
	rwInstanceLock.RUnlock()
	if ok {
		return rCache
	}

	configs := RedisConfigMap[ConfigNameMap[configName]]
	hash := ConsistentHash{200, 0, make(nodeSlice, 0), make(map[string][]position, 0)}

	nodes := make([]string, 0)
	// 需要跟php redis 逻辑修复到一致
	for _, v := range configs {
		nodes = append(nodes, v["master"])
	}
	hash.AddTargets(nodes)

	rCache = new(RedisCache)
	rCache.configs = configs
	rCache.hash = hash
	rCache.targetToPool = make(map[string][]*clientPoolUnit)
	rwInstanceLock.Lock()
	instances[configName] = rCache
	rwInstanceLock.Unlock()
	return rCache
}

func (r *RedisCache) getClient(key string) *clientPoolUnit {
	target := r.hash.Lookup(key)

	rwClientsLock.Lock()
	defer func() {
		rwClientsLock.Unlock()
	}()
	poolNodes, ok := r.targetToPool[target]

	// 目标链接
	var client *redis.Client

	if ok && len(poolNodes) > 0 {
		// 尝试拿一个空闲链接
		for _, poolUnit := range poolNodes {
			if !poolUnit.inUse {
				poolUnit.inUse = true
				return poolUnit
			}
			r.targetToPool[target] = poolNodes
		}
	}

	// 没有拿到链接，并且未到容量上限，那么新建一个链接push进池子并返回
	if client == nil && len(poolNodes) < connectionCapacity {
		client = redis.NewClient(&redis.Options{
			Addr: target,
		})
		poolUnit := &clientPoolUnit{true, client}
		r.targetToPool[target] = append(r.targetToPool[target], poolUnit)
		return poolUnit
	}

	begin := time.Now().Unix()
	for {
		for _, poolUnit := range poolNodes {
			if !poolUnit.inUse {
				client = poolUnit.client
				poolUnit.inUse = true
				r.targetToPool[target] = poolNodes
				return poolUnit
			}
			now := time.Now().Unix()
			if now-begin > 3 {
				return nil
			}
		}
	}
	return nil
}

func (r *RedisCache) Get(key string) (string, error) {
	c := r.getClient(key)

	result := c.client.Get(key)
	return result.Val(), result.Err()
}

func (r *RedisCache) Set(key string, val string, expire time.Duration) (bool, error) {
	c := r.getClient(key)
	if c == nil {
		return false, errors.New("client is nil")
	}
	res := c.client.Set(key, val, expire)
	c.release()
	result, error := res.Result()
	return result == "OK", error
}
